/*
 *  Copyright (c) 2000-2003 Yale University. All rights reserved.
 *
 *  THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE EXPRESSLY
 *  DISCLAIMED. IN NO EVENT SHALL YALE UNIVERSITY OR ITS EMPLOYEES BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED, THE COSTS OF
 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED IN ADVANCE OF THE POSSIBILITY OF SUCH
 *  DAMAGE.
 *
 *  Redistribution and use of this software in source or binary forms,
 *  with or without modification, are permitted, provided that the
 *  following conditions are met:
 *
 *  1. Any redistribution must include the above copyright notice and
 *  disclaimer and this list of conditions in any related documentation
 *  and, if feasible, in the redistributed software.
 *
 *  2. Any redistribution must include the acknowledgment, "This product
 *  includes software developed by Yale University," in any related
 *  documentation and, if feasible, in the redistributed software.
 *
 *  3. The names "Yale" and "Yale University" must not be used to endorse
 *  or promote products derived from this software.
 */

package edu.yale.its.tp.cas.client;

import java.io.IOException;
import java.io.StringReader;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import edu.yale.its.tp.cas.util.SecureURL;

/**
 * Validates STs and optionally retrieves PGT IOUs.
 * Designed with a bean-like interface for simplicity and generality.
 */
public class ServiceTicketValidator {

  //*********************************************************************
  // For testing...
  public static void main(String args[]) throws Exception {
    System.setProperty("java.protocol.handler.pkgs",
      "com.sun.net.ssl.internal.www.protocol");
    edu.yale.its.tp.cas.client.ServiceTicketValidator sv
      = new edu.yale.its.tp.cas.client.ServiceTicketValidator();
    sv.setCasValidateUrl("https://portal1.wss.yale.edu/cas/serviceValidate");
    sv.setProxyCallbackUrl("https://portal1.wss.yale.edu/casProxy/receptor");
    sv.setService(args[0]);
    sv.setServiceTicket(args[1]);
    sv.validate();
    System.out.println(sv.getResponse());
    System.out.println();
    if (sv.isAuthenticationSuccesful()) { 
      System.out.println("user: " + sv.getUser());
      System.out.println("pgtIou: " + sv.getPgtIou());
    } else {
      System.out.println("error code: " + sv.getErrorCode());
      System.out.println("error message: " + sv.getErrorMessage());
    }
  }


  //*********************************************************************
  // Private state

  private String casValidateUrl, proxyCallbackUrl, st, service, pgtIou,
    user, errorCode, errorMessage, entireResponse;
  private boolean renew = false;
  private boolean attemptedAuthentication;
  private boolean successfulAuthentication;

  /**
   * This is set to true when in demo mode, allowing the
   * validator to validate against non-HTTPs URLs
   * (in a broken security way).
   */
  private boolean brokenSecurityMode = false;

  //*********************************************************************
  // Accessors

  /**
   * Sets the CAS validation URL to use when validating tickets and
   * retrieving PGT IOUs.
   */
  public void setCasValidateUrl(String x) {
    this.casValidateUrl = x;
  }

  /**
   * Gets the CAS validation URL to use when validating tickets and
   * retrieving PGT IOUs.
   */
  public String getCasValidateUrl() {
    return this.casValidateUrl;
  }

  /**
   * Sets the callback URL, owned logically by the calling service, to
   * receive the PGTid/PGTiou mapping.
   */
  public void setProxyCallbackUrl(String x) {
    this.proxyCallbackUrl = x;
  }

  /**
   * Sets the "renew" flag on authentication.  When set to "true",
   * authentication will only succeed if this was an initial login
   * (forced by the "renew" flag being set on login).
   */
  public void setRenew(boolean b) {
    this.renew = b;
  }

  /**
   * Gets the callback URL, owned logically by the calling service, to
   * receive the PGTid/PGTiou mapping.
   */
  public String getProxyCallbackUrl() {
    return this.proxyCallbackUrl;
  }

  /**
   * Sets the ST to validate.
   */
  public void setServiceTicket(String x) {
    this.st = x;
  }

  /**
   * Sets the service to use when validating.
   */
  public void setService(String x) {
    this.service = x;
  }

  /**
   * Returns the strongly authenticated username.
   */
  public String getUser() {
    return this.user;
  }

  /**
   * Returns the PGT IOU returned by CAS.
   */
  public String getPgtIou() {
    return this.pgtIou;
  }

  /**
   * Returns <tt>true</tt> if the most recent authentication attempted
   * succeeded, <tt>false</tt> otherwise.
   */
  public boolean isAuthenticationSuccesful() {
    return this.successfulAuthentication;
  }

  /**
   * Returns an error message if CAS authentication failed.
   */
  public String getErrorMessage() {
    return this.errorMessage;
  }

  /**
   * Returns CAS's error code if authentication failed.
   */
  public String getErrorCode() {
    return this.errorCode;
  }

  /**
   * Retrieves CAS's entire response, if authentication was succsesful.
   */
  public String getResponse()  {
    return this.entireResponse;
  }


  //*********************************************************************
  // Actuator

  public void validate()
      throws IOException, SAXException, ParserConfigurationException {
    if (casValidateUrl == null || st == null)
      throw new IllegalStateException("must set validation URL and ticket");
    clear();
    attemptedAuthentication = true;
    StringBuffer sb = new StringBuffer();
    sb.append(casValidateUrl);
    if (casValidateUrl.indexOf('?') == -1)
      sb.append('?');
    else
      sb.append('&');
    sb.append("service=" + service + "&ticket=" + st);
    if (proxyCallbackUrl != null)
      sb.append("&pgtUrl=" + proxyCallbackUrl);
    if (renew)
      sb.append("&renew=true");
    String url = sb.toString();
    String response = SecureURL.retrieve(url, this.brokenSecurityMode);
    this.entireResponse = response;

    // parse the response and set appropriate properties
    if (response != null) {
      XMLReader r =
        SAXParserFactory.newInstance().newSAXParser().getXMLReader();
      r.setFeature("http://xml.org/sax/features/namespaces", false);
      r.setContentHandler(newHandler());
      r.parse(new InputSource(new StringReader(response)));
    }
  }


  //*********************************************************************
  // Response parser

  protected DefaultHandler newHandler() {
    return new Handler();
  }

  protected class Handler extends DefaultHandler {

    //**********************************************
    // Constants

    protected static final String AUTHENTICATION_SUCCESS = 
      "cas:authenticationSuccess";
    protected static final String AUTHENTICATION_FAILURE =
      "cas:authenticationFailure";
    protected static final String PROXY_GRANTING_TICKET =
      "cas:proxyGrantingTicket";
    protected static final String USER = "cas:user";

    //**********************************************
    // Parsing state

    protected StringBuffer currentText = new StringBuffer();
    protected boolean authenticationSuccess = false;
    protected boolean authenticationFailure = false;
    protected String netid, pgtIou, errorCode, errorMessage;
    

    //**********************************************
    // Parsing logic

    public void startElement(String ns, String ln, String qn, Attributes a) {
      // clear the buffer
      currentText = new StringBuffer();

      // check outer elements
      if (qn.equals(AUTHENTICATION_SUCCESS)) {
        authenticationSuccess = true;
      } else if (qn.equals(AUTHENTICATION_FAILURE)) {
        authenticationFailure = true;
        errorCode = a.getValue("code");
        if (errorCode != null)
          errorCode = errorCode.trim();
      }
    }

    public void characters(char[] ch, int start, int length) {
      // store the body, in stages if necessary
      currentText.append(ch, start, length);
    }

    public void endElement(String ns, String ln, String qn)
        throws SAXException {
      if (authenticationSuccess) {
        if (qn.equals(USER))
          user = currentText.toString().trim();
        if (qn.equals(PROXY_GRANTING_TICKET))
          pgtIou = currentText.toString().trim();
      } else if (authenticationFailure) {
        if (qn.equals(AUTHENTICATION_FAILURE))
          errorMessage = currentText.toString().trim();
      }
    }
 
    public void endDocument() throws SAXException {
      // save values as appropriate
      if (authenticationSuccess) {
        ServiceTicketValidator.this.user = user;
        ServiceTicketValidator.this.pgtIou = pgtIou;
        ServiceTicketValidator.this.successfulAuthentication = true;
      } else if (authenticationFailure) {
        ServiceTicketValidator.this.errorMessage = errorMessage;
        ServiceTicketValidator.this.errorCode = errorCode;
        ServiceTicketValidator.this.successfulAuthentication = false;
      } else
        throw new SAXException("no indication of success or failure from CAS");
    }
 }

  //*********************************************************************
  // Utility methods

  /**
   * Clears internally manufactured state.
   */
  protected void clear() {
   user = pgtIou = errorMessage = null;
   attemptedAuthentication = false;
   successfulAuthentication = false;
  }

/**
 * Is this ServiceTicketValidator configured
 * to pass renew=true on the ticket validation request?
 * @return true if renew=true on validation reqeust, false otherwise.
 */
public boolean isRenew() {
    return renew;
}

public String toString(){
	StringBuffer sb = new StringBuffer();
	sb.append("[");
	sb.append(ServiceTicketValidator.class.getName());
	if (brokenSecurityMode) {
		sb.append(" BROKEN SECURITY MODE SUITABLE ONLY FOR DEMO PURPOSES ");
	}
	if (casValidateUrl != null){
		sb.append(" casValidateUrl=[");
		sb.append(casValidateUrl);
		sb.append("]");
	}

	if (proxyCallbackUrl != null){
		sb.append(" proxyCallbackUrl=[");
		sb.append(proxyCallbackUrl);
		sb.append("]");
	}
	if (st != null){
		sb.append(" ticket=[");
		sb.append(st);
		sb.append("]");
	}
	if (service != null){
		sb.append(" service=[");
		sb.append(service);
		sb.append("]");
	}
	if (pgtIou != null){
		sb.append(" pgtIou=[");
		sb.append(pgtIou);
		sb.append("]");
	}
	if (user != null){
		sb.append(" user=[");
		sb.append(user);
		sb.append("]");
	}
	if (errorCode != null){
		sb.append(" errorCode=[");
		sb.append(errorCode);
		sb.append("]");
	}
	if (errorMessage != null){
		sb.append(" errorMessage=[");
		sb.append(errorMessage);
		sb.append("]");
	}
	sb.append(" renew=");
	sb.append(renew);
	if (entireResponse != null){
		sb.append(" entireResponse=[");
		sb.append(entireResponse);
		sb.append("]");
	}
	sb.append("]");
	return sb.toString();
		
}

/**
 * Returns true if this validator is configured for broken
 * not-SSL-requiring non-secure mode.  False in the normal
 * case of secure mode
 * @return true if demo mode, false otherwise
 */
public boolean getBrokenSecurityMode() {
	return brokenSecurityMode;
}

/**
 * Set whether the validator should be in broken security mode wherein
 * non-SSLed URLs are permitted.  Defaults to false -- when set to true
 * validator will allow insecure validation against a CAS server not secured with
 * SSL.
 * @param brokenSecurityMode
 */
public void setBrokenSecurityMode(boolean brokenSecurityMode) {
	this.brokenSecurityMode = brokenSecurityMode;
}

}
